Skip to content

前端项目中合理使用Babel

相信Web前端开发的小伙伴们,对Babel都不陌生了。几乎现代的稍微大一点儿的前端项目,都会用到它来帮我们编译JS(或TS)代码。 虽然经常接触,但是可能了解的并深入,有很多似懂非懂的地方。我们这里简要介绍一下有关前端项目引入Babel的注意点。

Babel 是啥?

按照Babel官方的说法,Babel 是一个JavaScript编译器。就是将现代的ECMAScript(或typescript)转成能支持特定宿主环境运行的 Javascript语言。 现在是 2023 年,ES的标准已经推行到了ES2023了,每年都会有很多新提案加入到ES的语言特性中,让 JavaScript 成为了一门支撑开发大型前端项目的通用语言。

Alt text

ECMAScript 是 JavaScript的语言规范,JavaScript是某一个平台(或环境)的实现。两者就是 普通话 和 方言的关系。

由于JavaScript主要是运行浏览器中的,浏览器安装在不同的设备上。在现在面临的问题是,还存在不少旧设备以及一些老的浏览器,它们不支持新的 ES语言特性,我们的产品又不能放弃这一部分的用户群体。 只能选择让项目代码尽可能地兼容老的设备或浏览器。那如何既能使用新的语言特性提升开放体验,又能兼顾老旧的浏览器和设备。

这时候Babel 就派上用场了。我们在开发环境下继续使用高级的ES语法特性,在打包到生产环境中的代码经过 Babel 编译,输出到特定平台所支持的 JS 版本。

再比如,我们在平时开发中写了React组件、Vue组件是不能直接在浏览器中运行的,需要进一步编译。

Alt text

Babel的运行原理

Babel的实现过程,大致经过解析转换生成这么三个阶段。

  • 扫描项目工程中的源代码目录,获得源码的输入
  • 将源代码,解析成AST(抽象语法树)
  • 根据需求,将解析得到的AST进一步加工转换,生成满足特定环境下的AST'
  • 将转换后的AST,生成目标代码,一般是输出到本地磁盘上的文件。

Alt text

需要注意的是,Babel只进行语言转换(比如const声明的变量、箭头函数等),而不提供新的语言API的支持(比如promise、Map、Set等),需要使用相关的插件来满足需求。

而Babel的架构模式,正式基于插件的架构模式。将拓展的功能,从核心模块中抽离出来,形成一系列的插件。 既降低了框架的复杂度,也提升了架构的灵活程度。核心模块core包 和 插件 plugin 包 ,以松散的方式耦合,两者在功能不变的情况下,可以独立发布,互相补充影响。并且公开了插件的接口,让开发者也能对插件进行扩展。

Babel的插件

大致分为两类:语法插件(Syntax Plugins) 和 转换插件。

  • 语法插件:解析特定类型的语法。它们能够帮助构造抽象语法树(AST)。典型的语法插件有:syntax-async-functions 以及 syntax-jsx
  • 转换插件:其作用为修改抽象语法树。典型的转换插件有:transform-async-to-generator、transform-react-jsx、transform-es2015-arrow-functions 等

presets 预设

babel中的 presets 是一些列Babel插件的集合。因为Babel的插件众多,使用的时候一个个配置,太过繁琐了。 有了presets 方便用户对插件的使用和管理。

Babel 官方为我们提供了一些常见的 preset

  • @babel/preset-env:转换、编译 ES2015+ 的语法插件集;
  • @babel/preset-flow:为 Flow 提供的预设,包含所有 flow 相关的插件;
  • @babel/preset-react:为 React 提供的预设,包含所有 React 相关的插件;
  • @babel/preset-typescript:为 TS 提供的预设,包含所有 TS 相关的插件;

我们平时使用到的是 @babel/preset-env ,这个预设会随着 ECMA 规范的更新增加自身的内容。那么这样的问题就是,随着时间的推移,该预设的内容会越来越多,编译的速度也会越来越慢。并且随着时间的推移,冗余的插件也会越来越多。所以,目前 Babel 官方不再推出 babel-preset-es2017 以后的预设了。

如果使用默认配置,那么就会和 babel-preset-latest 预设相同,会加载从 ES2015 开始的所有 preset。如果我们想根据业务项目的需求,设定运行的环境,来定制输出的代码。可以传入target 参数,例如,支持最近的 3个浏览器版本和 安卓4.4 版本以及 iOS 9.0 以上版本运行我们的代码,那么可以按照如下配置:

json
presets: [
   [
     '@babel/preset-env',
     {
       targets: {
         "browsers": [
           "last 3 versions",
           "Android >= 4.4",
           "iOS >= 9.0"
         ],
       }
     },
   ],
 ]
presets: [
   [
     '@babel/preset-env',
     {
       targets: {
         "browsers": [
           "last 3 versions",
           "Android >= 4.4",
           "iOS >= 9.0"
         ],
       }
     },
   ],
 ]

搭配 Polyfill 使用

由于 Babel 默认在编译时只会转换新的 JavaScript 语法(syntax),但不会转换 API,比如 Set、Maps、Generator、Proxy、Promise 等全局对象,以及一些定义在全局对象上的方法(比如Array.from、Object.assign)都不会被转译。因此 Babel 官方推出了 @babel/polyfill 库。

其核心依赖是 core-js@2 和 regenerater-runtime/runtime。core-js 是 JS 标准库的 polyfill,为其提供垫片能力,regenerater-runtime/runtime 用来转译 generators 和 async 函数。

core-js 提供对各种ES6+ API的 polyfill

Alt text

regenerater-runtime/runtime 转译 generators 和 async 函数

Alt text

core-js 是一个 JavaScript 标准库,里面包含了 ESCAScreipt 2020 在内的多项特性的 polyfill。由于包含的功能比较多,导致其体积较大(有2M之多)。

后来推出了core-js@3,使用 Monorepo 进行拆包,拆成了 5 个相关的包:

  • core-js:是整个 core-js 的核心,提供了基础的垫片能力,但是直接使用 core-js 会污染全局命名空间和对象原型;
  • core-js-pure:core-js-pure 提供了独立的命名空间,不污染全局变量;
  • core-js-compact:根据 Browserslist 维护了不同宿主环境、不同版本下对应需要支持特性的集合;
  • core-js-builder:结合 core-js-compact 以及 core-js,并利用 webpack 能力,根据需求打包出 core-js
  • core-js-bundle

但是,如果我们在项目中直接安装 @babel/polyfill,会看到如的警告提示:

Alt text

@babel/polyfill 将被弃用,已经不再推荐使用了。那么该如何使用babel-polyfill的功能呢?

单独使用

如果不依赖前端构建工具单独使用的话,需要安装依赖 npm install --save core-js regenerator-runtime,然后需要在业务代码中需要进行引入:

js
import "core-js/stable";
import "regenerator-runtime/runtime";
import "core-js/stable";
import "regenerator-runtime/runtime";

注意:此时不再引入@babel/polyfill 这个包了,因为 @bable/polyfill 也是依赖 core-js 并且会锁死 2.x 版本。

配合webpack使用

更改 webpack 的配置文件中的 entry 配置:

js
// webpack.config.js
const path = require('path');
module.exports = {
  entry: ['core-js/stable', 'regenerator-runtime/runtime', './main.js'],
  output: {
    filename: 'dist.js',
    path: path.resolve(__dirname, '')
  },
  mode: 'development'
};
// webpack.config.js
const path = require('path');
module.exports = {
  entry: ['core-js/stable', 'regenerator-runtime/runtime', './main.js'],
  output: {
    filename: 'dist.js',
    path: path.resolve(__dirname, '')
  },
  mode: 'development'
};

@babel/preset-env

上面的解决方案中,是将垫片全量进行引入的,完整的 polyfills 文件非常大,不利于我们打包出来的体积和页面的性能。

下面我们 使用 Babel 的预设或者插件做到按需使用,也是现在项目开发中主流的使用方式,使用 @babel/preset-env 预设。

@babel/preset-env 这个预设包含所有标准的最新特性,转换那些已经被正式纳入 TC39 中的语法;该预设在 Babel6 的时候的名字是 babel-preset-env 在 Babel7 后,更名为 @babel/preset-env,该预设不只可以在编译时通过转换 AST 来进行语法转换,还有一个重要功能就是根据设置的参数针对性处理 polyfill,真正做到按需引入。

最基本的配置:

js
module.exports = {
  presets: ["@babel/preset-env"],
  plugins: []
}
module.exports = {
  presets: ["@babel/preset-env"],
  plugins: []
}

配置 targets 选项,支持 3个 版本的浏览器和 安卓4.4 以上的系统以及 iOS 9.0 以上的系统:

js
module.exports = {
  presets: [["@babel/preset-env", {
  	targets: {
      browsers: [
        'last 3 versions',
        'Android >= 4.4',
        'iOS >= 9.0',
      ],
    },
  ]],
  plugins: []
}
module.exports = {
  presets: [["@babel/preset-env", {
  	targets: {
      browsers: [
        'last 3 versions',
        'Android >= 4.4',
        'iOS >= 9.0',
      ],
    },
  ]],
  plugins: []
}

useBuiltIns

useBuiltIns 配置决定了 @babel/preset-env 该如何处理 polyfill。其选项值:"usage" 、"entry" 和 false, 默认为 false。

false

如果使用默认的 false,polyfill 就不会被按需处理会被全部引入

entry

设置为 entry,需要手动导入 @babel/polyfill,可以直接导入 core-js 和 regenerator-runtime 也可以在 webpack 的 entry 中设置。useBuiltIn: entry 的作用就是会自动将import "core-js/stable" 和 import "regenerator-runtime/runtime" 转换为目标环境的按需引入。

js
module.exports = {
  presets: [["@babel/preset-env", {
  	useBuiltIns: "entry",
  ]],
  plugins: []
}
module.exports = {
  presets: [["@babel/preset-env", {
  	useBuiltIns: "entry",
  ]],
  plugins: []
}

entry配置只针对目标环境,而不是具体代码,所以 Babel 会针对目标环境引入所有的 polyfill 扩展包,用不到的polyfill也可能会引入进来。所以,如果不需要考虑打包产物的大小,可以使用该配置。

usage

useBuiltIns 设置为 usage,则不需要手动导入 polyfill,babel 检测出此配置会自动进行 polyfill 的引入。其配置如下:

js
module.exports = {
  presets: [["@babel/preset-env", {
  	useBuiltIns: "usage",
  ]],
  plugins: []
}
module.exports = {
  presets: [["@babel/preset-env", {
  	useBuiltIns: "usage",
  ]],
  plugins: []
}

usage 模式下,Babel 除了会针对目标环境引入 polyfill 的同时也会考虑项目代码代码中使用了哪些 ES6+ 的新特性,两者取一个最小的集合作为 polyfill 的导入。

所以,如果希望打包出的代码尽可能的精简,那么 usage 模式是一个不错的选择,并且这也是官方推荐的使用方式。

Babel 配置

@babel/preset-env 预设也可以让你自己选择需要使用 2 还是 3。并且这个参数只有 useBuiltIn 设置为 usage 或者 entry 时才会生效。

该配置默认值为 2,但是如果我们需要某些最新的 API 时,需要将其设置为 3。

@babel/runtime

@babel/runtime 是含有 babel 编译所需要的一些 helpers 函数。同时还提供了 regenerator-runtime,对 generator 和 async函数进行编译降级。

Babel 在转译 syntax 时,有时候会使用一些辅助的函数来帮忙,比如我们需要转译 class 类:

Alt text

class 语法的转换过程中, @babel/preset-env 自定义了 _classCallCheck 这个函数来辅助转换。这个函数就是 helper 函数。这是 @babel/preset-env 在做语法转换的时候,注入了这些 helpers 函数声明,以便语法转换后使用。

helper 函数在转译后的文件中被定义了一遍。也就是说,项目中有多少个文件中存在需要转换的 class,那么在打包的产物中就会有多少个 _classCallCheck helper 函数,这显然不是一种高效的做法。

解决思路是将这些 helpers 函数都放入到某个依赖包中,在使用的时候直接从该包中引入即可,这样打包出来的产物中,就只有一份 helpers 函数。上面提到的 @babel/runtime 就是这个依赖包。

@babel/plugin-transform-runtime

@babel/plugin-transform-runtime 是帮我们用工程化的手段解决来解决问题的。我们使用 @babel/plugin-transform-runtime 自动将需要引入的 helpers 函数替换为 @babel/runtime 中的引用。

Alt text

@babel/plugin-transform-runtime 还有另一个关键的作用就是对 API 进行转换的时候,避免污染全局变量。

@babel/polyfill 的处理机制是,对于例如 Array.from 等静态方法,直接在 global.Array 上添加;对于例如 includes 等实例方法,直接在 global.Array.prototype 上添加。

直接修改了全局变量的原型,造成全局污染的问题。在作为三方的插件使用,有可能引发冲突问题。

@babel/plugin-transform-runtime 将 Promise 转换为 _promise["default"],而 _promise["default"] 拥有ES标准里 Promise 所有的功能。现在,即使浏览器没有 Promise,我们的代码也能正常运行。

js
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var obj = _promise["default"].resolve();
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var obj = _promise["default"].resolve();

因此,transform-runtime 插件的主要作用:

  1. 直接将 helpers 从文件中定义改为从 @babel/runtime 中引入,避免了多次引入 helpers 辅助函数。
  2. 将 @babel/ployfill 中 API 的 polyfill 直接修改原型改为从 @babel/runtime-corejs3/helpers中获取,避免对全局变量和原型的污染。

总结

本文简要介绍了 Babel 的作用,Babel的原理,Babel的插件,尤其是 @babel/preset-env插件预设。 Babel的设计理念,如何处理polyfill,@babel/polyfill废弃的原因。如何使用 @babel/preset-env 结合 usage,生成项目所需的polyfill。 重点介绍了Babel在项目使用时的注意要点,尤其是如何合理配置polyfill,实现高质量打包代码的输出。

相关链接